﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using Wombat.Core.Services.Pattern;

namespace Wombat.Core.Services
{
    public class ObservationManager : IObservationManager
    {
        private readonly ITagRpository _tagRpository;
        private readonly IDiagnosticsService _diagnosticsService;

        private readonly ConcurrentDictionary<Guid, ConcurrentDictionary<Guid, IObservableProxy>> _observationMap
            = new ConcurrentDictionary<Guid, ConcurrentDictionary<Guid, IObservableProxy>>();
        private readonly IDictionary<Guid, Type> _registrations 
            = new ConcurrentDictionary<Guid, Type>();

        public ObservationManager(ITagRpository tagRpository, IDiagnosticsService diagnosticsService)
        {
            _tagRpository = tagRpository;
            _diagnosticsService = diagnosticsService;
        }

        #region IObservationManager implementaion

        public IDisposable Subscribe<T>(IUniqueIdentifiable publisherPort, string pattern, IObserver<T> observer)
            where T : ITagValue
        {
            Contract.Assert(publisherPort != null && publisherPort.Guid != null && publisherPort.Guid != Guid.Empty);

            if (!_registrations.ContainsKey(publisherPort.Guid) || !typeof(T).IsAssignableFrom(_registrations[publisherPort.Guid]))
                throw new InvalidOperationException(string.Format("Port '{0}' is not registered or is not publishing data type '{1}'", publisherPort.Guid, typeof(T)));

            var observable = FindOrCreateObservableNode<T>(publisherPort, pattern);
            if (observable == null)
                throw new InvalidOperationException(string.Format("Failed to obsereve port '{0}' pattern '{1}'",
                                                                  publisherPort.Guid, pattern));
            return observable.Subscribe(observer);
        }

        public void Register<T>(IUniqueIdentifiable publisherPort) where T : ITagValue
        {
            var portDataType = typeof (T);

            if (_registrations.ContainsKey(publisherPort.Guid))
                throw new InvalidOperationException(string.Format("Port with id {0} is already registered.", publisherPort.Guid));
            _registrations.Add(publisherPort.Guid, portDataType);
        }

        public void OnNext<T>(IUniqueIdentifiable publisher, T data) where T: ITagValue
        {
            Contract.Assert(publisher != null);
            Contract.Assert(publisher.Guid != null);

            foreach (var observableProxy in Subscribers(publisher, _tagRpository.Get(data.TagId)))
            {
                try
                {
                    ((ObservableProxy<T>)observableProxy).OnNext(data);
                }
                catch (AggregateException exceptions)
                {
                    _diagnosticsService.WriteFault(exceptions.ToString());
                }
            }
        }

        public void OnError(IUniqueIdentifiable publisher, Exception error)
        {
            Contract.Assert(publisher != null);
            Contract.Assert(publisher.Guid != null);

            foreach (var observableProxy in Subscribers(publisher, null))
            {
                observableProxy.OnError(error);
            }
        }

        public void OnCompleted(IUniqueIdentifiable publisher)
        {
            Contract.Assert(publisher != null);
            Contract.Assert(publisher.Guid != null);

            foreach (var observableProxy in Subscribers(publisher, null))
            {
                observableProxy.OnCompleted();
            }
        }

        #endregion


        private IObservable<T> FindOrCreateObservableNode<T>(IUniqueIdentifiable publisher, string pattern) where T: ITagValue
        {
            if (!_registrations.ContainsKey(publisher.Guid))
                throw new InvalidOperationException(string.Format("Error trying to subscribe to gate '{0}' which is not refistered with ObservationManager.", publisher.Guid));
            var patternId = PatternDefinition.GetPatternType(pattern).GetGuid();
            var mapping = _observationMap.GetOrAdd(publisher.Guid, new ConcurrentDictionary<Guid, IObservableProxy>());
            return (IObservable<T>)mapping.GetOrAdd(patternId, g => new ObservableProxy<T>());
        }

        private IEnumerable<IObservableProxy> Subscribers(IUniqueIdentifiable publisher, ITag tag)
        {
            //This method is build on the assumption that only a whildcard '*' or a
            //patten made of the tag IDs is acceptable.
            //In order to support more complex patterns, this method must be changed.
            if (!_observationMap.ContainsKey(publisher.Guid)) yield break;
            var subscribedPatterns = _observationMap[publisher.Guid];
            
            if (tag == null)
            {
                foreach (var observer in subscribedPatterns.Values)
                {
                    yield return observer;
                }
                yield break;
            }

            //The pattern guid is same as the tag guid.
            //For example one may want to listen only to one tag, so he should provide the 
            //appropriate tag id.
            if (subscribedPatterns.ContainsKey(tag.UniqueId))
                yield return subscribedPatterns[tag.UniqueId];
            //A wildcard can also exist for a subscription.
            if (subscribedPatterns.ContainsKey(AnythingPatternDefinition.WildCardGuid))
                yield return subscribedPatterns[AnythingPatternDefinition.WildCardGuid];
        }
    }
}